
package de.justphil.tcg.tcgserver.game.mechanics.game;

import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
import java.util.LinkedList;
import java.util.List;

import com.hazelcast.nio.DataSerializable;

import de.justphil.tcg.tcgserver.commons.exceptions.TCGException;
import de.justphil.tcg.tcgserver.game.mechanics.config.Config;
import de.justphil.tcg.tcgserver.game.mechanics.event.Event;
import de.justphil.tcg.tcgserver.game.mechanics.event.EventManager;
import de.justphil.tcg.tcgserver.game.mechanics.event.EventObservable;
import de.justphil.tcg.tcgserver.game.mechanics.event.OnEventListener;
import de.justphil.tcg.tcgserver.game.mechanics.event.Scheduler;
import de.justphil.tcg.tcgserver.game.mechanics.monster.Monster;
import de.justphil.tcg.tcgserver.game.mechanics.monster.MonsterList;
import de.justphil.tcg.tcgserver.game.mechanics.property.Property;
import de.justphil.tcg.tcgserver.game.mechanics.property.PropertyList;
import de.justphil.tcg.tcgserver.game.mechanics.util.tools.IterationHelper;

import org.slf4j.Logger;
import static de.justphil.tcg.tcgserver.commons.logging.TCGLogging.*;

public class Game implements OnEventListener, OnUserInteractionListener, GameProgressObservable,
		OnMonsterProgressListener, DataSerializable {
	
	/**
	 * 
	 */
	private static final long				serialVersionUID	= 5122951303833046653L;
	private static final Logger				log					= gameLogger(Game.class);
	private List<Monster>[]					decks;
	private Monster							turnMonster;
	private GameBoard						gameBoard;
	private EventManager					eventManager;
	private boolean							waitingOnSlotSelection;
	private GameRandom						random;
	private List<OnGameProgressListener>	onGameProgressListeners;
	private DataSerializableManager			dataSerializableManager;
	
	/**
	 * Constructor!
	 */
	public Game(MonsterList[] decks) {
	
		this(Config.SEED, decks);
	}
	
	/**
	 * Constructor for deserialization
	 */
	public Game() {
	
		eventManager = new EventManager();
		registerEvents();
		gameBoard = new GameBoard();
		onGameProgressListeners = new LinkedList<OnGameProgressListener>();
		// ###############################
		random = new GameRandom();
		decks = new MonsterList[Config.NUM_PLAYERS];
	}
	
	public Game(long seed, MonsterList[] decks) {
	
		eventManager = new EventManager();
		registerEvents();
		gameBoard = new GameBoard();
		onGameProgressListeners = new LinkedList<OnGameProgressListener>();
		random = new GameRandom(seed);
		waitingOnSlotSelection = false;
		scheduleEnd(Config.MAX_TIME_WITHOUT_DEATH);
		setDecks(decks);
	}
	
	public EventObservable getEventObservable() {
	
		return this.eventManager;
	}
	
	public Scheduler getScheduler() {
	
		return this.eventManager;
	}
	
	public BoardInfo getBoardInfo() {
	
		return gameBoard;
	}
	
	public GameRandom getGameRandom() {
	
		return random;
	}
	
	public DataSerializableManager getDataSerializableManager() {
	
		return dataSerializableManager;
	}
	
	public void setDecks(List<Monster>[] decks) {
	
		if (decks.length != Config.NUM_PLAYERS) {
			assert false;
			return;
		}
		this.decks = decks;
		/* Setting the Game to all Monsters */
		for (int player = 0; player < Config.NUM_PLAYERS; player++) {
			for (Monster monster : decks[player]) {
				monster.setGame(this);
			}
		}
		/* Filling the game board. */
		for (int player = 0; player < Config.NUM_PLAYERS; player++) {
			for (int slot = 0; slot < Config.NUM_SLOTS_PER_PLAYER; slot++) {
				drawNewMonster(new BoardPosition(player, slot));
			}
		}
	}
	
	@Override
	public void onSlotSelected(int player, int slot) {
	
		if (waitingOnSlotSelection) {
			BoardPosition position = new BoardPosition(player, slot);
			Monster target;
			target = findTargetMonster(position);
			if (target != null) {
				log.info("<- Slot ({}) selected.", new Object[] {
					position });
				waitingOnSlotSelection = false;
				scheduleAttack(target);
				turnMonster = null;
				eventManager.run();
			}
			else {
				log.warn("<- Slot ({}) is invalid!.", new Object[] {
					position });
				BoardPosition turnPosition = gameBoard.getBoardPosition(turnMonster);
				onSelectionRequired(turnPosition.getPlayer(), turnPosition.getSlot());
			}
		}
		else {
			log.warn("Trying to set the slot selection although confirm is required! Confirming instead!");
			eventManager.run();
		}
	}
	
	@Override
	public void onResume() {
	
		log.info("<- Confirmation.");
		if (!waitingOnSlotSelection) {
			eventManager.run();
		}
		else {
			log.warn("Trying to confirm although a slot selection is required!");
		}
	}
	
	@Override
	public void onMonsterChanged(Monster monster) {
	
		if (monster == null) {
			log.warn("Trying to signal a null Monster!");
			return;
		}
		final BoardPosition position = gameBoard.getBoardPosition(monster);
		final PropertyList properties = (PropertyList) monster.getProperties().clone();
		new IterationHelper<Property>(properties) {
			
			@Override
			protected void execute(Property listElement) {
			
				if (!listElement.isActive()) {
					properties.remove(listElement);
				}
			}
		}.safeExecuteForAll();
		int[] propertyTypes = new int[properties.size()];
		for (int i = 0; i < properties.size(); i++) {
			propertyTypes[i] = properties.get(i).getType();
		}
		onProgress(position.getPlayer(), position.getSlot(), monster.getId(), propertyTypes,
				monster.getLife(), monster.getAttack(), monster.getDefense(), monster.getSpeed());
	}
	
	@Override
	public void onEvent(Event event) {
	
		switch (event.getType()) {
			case Event.EVENT_PROPERTY_ACTIVATED:
				handleMonsterPropertiesChanged((Monster) event.getData());
				break;
			case Event.EVENT_PROPERTY_DEACTIVATED:
				handleMonsterPropertiesChanged((Monster) event.getData());
				break;
			case Event.EVENT_MONSTERS_TURN:
				handleMonstersTurn((Monster) event.getData());
				break;
			case Event.EVENT_REMOVE_IF_DEAD:
				handleRemoveIfDead((Monster) event.getData());
				break;
			case Event.EVENT_DRAW_NEW_MONSTER:
				handleDrawNewMonster((BoardPosition) event.getData());
				break;
			case Event.EVENT_END:
				handleEnd();
				break;
			case Event.EVENT_START:
				handleStart();
				break;
			default:
				break;
		}
	}
	
	@Override
	public void addOnGameProgressListener(OnGameProgressListener onGameProgressListener,
			boolean update) {
	
		if (onGameProgressListeners.contains(onGameProgressListener)) {
			log.warn("Trying to add the same OnEventListener twice!");
		}
		else {
			onGameProgressListeners.add(onGameProgressListener);
			if (update) {
				updateAllSlots(onGameProgressListener);
			}
		}
	}
	
	@Override
	public boolean removeOnGameProgressListener(OnGameProgressListener onGameProgressListener) {
	
		return onGameProgressListeners.remove(onGameProgressListener);
	}
	
	@Override
	public void writeData(DataOutput out) throws IOException {
	
		log.info("Serializing the Game.", new Object[] {});
		if (dataSerializableManager == null) {
			dataSerializableManager = new DataSerializableManager();
		}
		writeData(null, out);
	}
	
	@Override
	public void readData(DataInput in) throws IOException {
	
		log.info("Deserializing the Game.", new Object[] {});
		dataSerializableManager = new DataSerializableManager();
		readData(null, in);
	}
	
	private void registerEvents() {
	
		eventManager.addOnEventListener(Event.EVENT_PROPERTY_ACTIVATED, this);
		eventManager.addOnEventListener(Event.EVENT_PROPERTY_DEACTIVATED, this);
		eventManager.addOnEventListener(Event.EVENT_START, this);
		eventManager.addOnEventListener(Event.EVENT_MONSTERS_TURN, this);
		eventManager.addOnEventListener(Event.EVENT_REMOVE_IF_DEAD, this);
		eventManager.addOnEventListener(Event.EVENT_DRAW_NEW_MONSTER, this);
		eventManager.addOnEventListener(Event.EVENT_END, this);
	}

	private void updateAllSlots(OnGameProgressListener listener) {
	
		for (int player = 0; player < Config.NUM_PLAYERS; player++) {
			for (int slot = 0; slot < Config.NUM_SLOTS_PER_PLAYER; slot++) {
				Monster monster = gameBoard.getMonster(new BoardPosition(player, slot));
				if (monster != null) {
					onMonsterChanged(monster);
				}
			}
		}
	}

	private void updateAllSlots() {
		
		for(OnGameProgressListener listener: onGameProgressListeners){
			
			updateAllSlots(listener);
		}
	}
	private void scheduleEnd(double time) {
	
		try {
			eventManager.scheduleLast(new Event(Event.EVENT_END, null), time);
		}
		catch (TCGException e) {
			assert false;
		}
	}
	
	private void rescheduleEnd(double time) {
	
		if (!eventManager.unschedule(new Event(Event.EVENT_END, null))) {
			assert false;
		}
		scheduleEnd(time);
	}
	
	private void handleDrawNewMonster(BoardPosition position) {
	
		if (!drawNewMonster(position)) {
			/* If any player is without cards the game will end. */
			if (gameBoard.getMonsters(0).size() == 0 || gameBoard.getMonsters(1).size() == 0) {
				log.info("Ending the game.", new Object[] {});
				rescheduleEnd(0.0);
			}
			else {
				rescheduleEnd(Config.MAX_TIME_WITHOUT_DEATH);
			}
		}
	}
	
	private void handleMonsterPropertiesChanged(Monster monster) {
	
		// TODO: isDeaktivationInProgress will probably not be needed any more.
		if (monster.isActive() && !monster.isDeactivationInProgress()) {
			Event removeMonster = new Event(Event.EVENT_REMOVE_IF_DEAD, monster);
			if (monster.getLife() <= 0 && !(eventManager.getTimeToEvent(removeMonster) != -1)) {
				try {
					this.eventManager.scheduleFirst(new Event(Event.EVENT_MONSTER_DIED, monster),
							0.0);
				}
				catch (TCGException e) {
					assert false : e.getMessage();
				}
				try {
					this.eventManager.scheduleLast(removeMonster, 0.0);
				}
				catch (TCGException e) {
					assert false : e.getMessage();
				}
			}
		}
	}
	
	private void handleRemoveIfDead(Monster monster) {
	
		/* Checking if the monster has been resurrected or whatever... */
		if (!(monster.getLife() > 0)) {
			int player = gameBoard.getBoardPosition(monster).getPlayer();
			removeMonster(monster);
			onConfirmRequired(player);
		}
	}
	
	/**
	 * Works only for two players!!
	 */
	private void handleEnd() {
	
		if (gameBoard.getAll().size() == 0) {
			onGameOver(OnGameProgressListener.DRAW);
		}
		else if (gameBoard.getMonsters(0).size() == 0) {
			onGameOver(1);
		}
		else if (gameBoard.getMonsters(1).size() == 0) {
			onGameOver(0);
		}
		else {
			onGameOver(OnGameProgressListener.DRAW);
		}
	}
	private void handleStart() {

		updateAllSlots();
	}
	
	
	private void handleMonstersTurn(Monster monster) {
	
		scheduleMonster(monster);
		turnMonster = monster;
		BoardPosition turnPosition = gameBoard.getBoardPosition(monster);
		if (turnPosition == null) {
			log.warn("The monster ({}) at turn is not on the game board!", new Object[] {
				monster });
			return;
		}
		int turnPlayer = turnPosition.getPlayer();
		int turnPlayerSlot = turnPosition.getSlot();
		onSelectionRequired(turnPlayer, turnPlayerSlot);
	}
	
	/**
	 * Draws a random Monster from the players deck places it on the specified slot on the
	 * gameBoard.
	 */
	private boolean drawNewMonster(BoardPosition position) {
	
		int cardsLeft = this.decks[position.getPlayer()].size();
		if (cardsLeft == 0) {
			return false;
		}
		int cardNumber;
		if (cardsLeft == 1) {
			cardNumber = 0;
		}
		else {
			cardNumber = random.nextInt(cardsLeft - 1);
		}
		Monster monster = this.decks[position.getPlayer()].remove(cardNumber);
		placeMonster(monster, position);
		return true;
	}
	
	private void placeMonster(Monster monster, BoardPosition position) {
	
		gameBoard.setSlot(monster, position);
		scheduleMonster(monster);
		onMonsterChanged(monster);
		monster.activate();
		/* Refreshing Display. This must come after the monster's activation! */
		log.info("Placed a new Monster ({}) on ({}).", new Object[] {
				monster, position });
		try {
			eventManager.scheduleLast(new Event(Event.EVENT_MONSTER_PLACED, monster), 0.0);
		}
		catch (TCGException e) {
			assert false : e.getMessage();
		}
	}
	
	private void removeMonster(Monster monster) {
	
		monster.deactivate();
		boolean removed = false;
		for (int player = 0; player < Config.NUM_PLAYERS; player++) {
			for (int slot = 0; slot < Config.NUM_SLOTS_PER_PLAYER; slot++) {
				BoardPosition position = new BoardPosition(player, slot);
				if (gameBoard.getMonster(position) == monster) {
					eventManager.unschedule(new Event(Event.EVENT_MONSTERS_TURN, monster));
					gameBoard.clearSlot(position);
					signalSlotCleared(position);
					removed = true;
					log.info("Removed a Monster ({}) from ({}).", new Object[] {
							monster, position });
					try {
						eventManager.scheduleLast(new Event(Event.EVENT_MONSTER_REMOVED, monster),
								0.0);
					}
					catch (TCGException e) {
						assert false : e.getMessage();
					}
					try {
						eventManager.scheduleLast(
								new Event(Event.EVENT_DRAW_NEW_MONSTER, position), 0.0);
					}
					catch (TCGException e) {
						assert false : e.getMessage();
					}
					player = Config.NUM_PLAYERS;
					break;
				}
			}
			if (removed) {
				break;
			}
		}
	}
	
	private Monster findTargetMonster(BoardPosition position) {
	
		if (turnMonster == null) {
			log.warn("Could not attack position ({})! No Monster has turn!", new Object[] {
				position });
			return null;
		}
		Monster target = gameBoard.getMonster(position);
		MonsterList enemies = gameBoard.getEnemies(turnMonster);
		BoardPosition turnPosition = gameBoard.getBoardPosition(turnMonster);
		assert enemies.size() != 0 : "The monster at position (" + position + ") has no enemies!";
		for (Monster enemy : enemies) {
			if (enemy == target) {
				return target;
			}
		}
		log.warn("Could not attack position ({})! The target is not an enemy of the position ({})",
				new Object[] {
						position, turnPosition });
		return null;
	}
	
	private void scheduleAttack(Monster target) {
	
		Attack attack = new Attack(turnMonster, target);
		try {
			eventManager.scheduleLast(new Event(Event.EVENT_ATTACK_INVOKED, attack), 0.0);
		}
		catch (TCGException e) {
			assert false : e.getMessage();
		}
		try {
			eventManager.scheduleLast(new Event(Event.EVENT_ATTACK, attack), 0.0);
		}
		catch (TCGException e) {
			assert false : e.getMessage();
		}
		try {
			eventManager.scheduleLast(new Event(Event.EVENT_ATTACK_FINISHED, attack), 0.0);
		}
		catch (TCGException e) {
			assert false : e.getMessage();
		}
	}
	
	private void signalSlotCleared(BoardPosition position) {
	
		onProgress(position.getPlayer(), position.getSlot(), -1, null, 0, 0, 0, 0);
	}
	
	private void scheduleMonster(Monster monster) {
	
		double additionalTime = random.nextDouble() * Config.MONSTER_SCHEDULE_VARIANCE;
		try {
			eventManager.scheduleLast(new Event(Event.EVENT_MONSTERS_TURN, monster),
					Config.STANDARD_MONSTER_ROUND_TIME + additionalTime);
		}
		catch (TCGException e) {
			assert false : e.getMessage();
		}
	}
	
	private void onGameOver(int winner) {
	
		log.warn("-> GameOver with winner {}.", winner);
		for (OnGameProgressListener listener : onGameProgressListeners) {
			listener.onGameOver(winner);
		}
		eventManager.stop();
	}
	
	private void onProgress(int player, int slot, long cardId, int[] propertyTypes, int life,
			int attack, int defense, int agility) {
	
		log.debug("-> Progress");
		for (OnGameProgressListener listener : onGameProgressListeners) {
			listener.onProgress(player, slot, cardId, propertyTypes, life, attack, defense, agility);
		}
		/* Requiring a confirmation each time a slot is changed! */
		// onConfirmRequired(player);
	}
	
	private void onSelectionRequired(int player, int slot) {
	
		log.info("-> Selection Required...");
		waitingOnSlotSelection = true;
		eventManager.stop();
		for (OnGameProgressListener listener : onGameProgressListeners) {
			listener.onSelectionRequired(player, slot);
		}
	}
	
	private void onConfirmRequired(int player) {
	
		log.info("-> Confirm required...");
		eventManager.stop();
		for (OnGameProgressListener listener : onGameProgressListeners) {
			listener.onResumeRequired(player);
		}
	}
	
	private void writeData(Game game, DataOutput out) throws IOException {
	
		writePrimitives(out);
		writeObjects(this, out);
	}
	
	private void readData(Game game, DataInput in) throws IOException {
	
		readPrimitives(in);
		readObjects(this, in);
	}
	
	private void writePrimitives(DataOutput out) throws IOException {
	
		out.writeBoolean(waitingOnSlotSelection);
		// out.writeBoolean(decksSet);
	}
	
	private void readPrimitives(DataInput in) throws IOException {
	
		waitingOnSlotSelection = in.readBoolean();
		// decksSet = in.readBoolean();
	}
	
	private void writeObjects(Game game, DataOutput out) throws IOException {
	
		writeDecks(game, out);
		random.writeData(out);
		dataSerializableManager.writeMonster(turnMonster, game, out);
		gameBoard.writeData(game, out);
		eventManager.writeData(game, out);
	}
	
	private void readObjects(Game game, DataInput in) throws IOException {
	
		readDecks(game, in);
		random.readData(in);
		turnMonster = dataSerializableManager.readMonster(game, in);
		gameBoard.readData(game, in);
		eventManager.readData(game, in);
	}
	
	private void writeDecks(Game game, DataOutput out) throws IOException {
	
		out.writeInt(decks.length);
		for (int i = 0; i < decks.length; i++) {
			out.writeInt(decks[i].size());
			for (Monster m : decks[i]) {
				dataSerializableManager.writeMonster(m, game, out);
			}
		}
	}
	
	private void readDecks(Game game, DataInput in) throws IOException {
	
		int playerSize = in.readInt();
		MonsterList ml = null;
		int monsterSize;
		for (int i = 0; i < playerSize; i++) {
			monsterSize = in.readInt();
			ml = new MonsterList();
			for (int j = 0; j < monsterSize; j++) {
				ml.add(dataSerializableManager.readMonster(game, in));
			}
			decks[i] = ml;
		}
	}
}
